{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true,
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "附加项目1:多线程/多进程基础\n",
    "=="
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "多线程类似于同时执行多个不同程序，多线程运行有如下优点：\n",
    "\n",
    "<font face=\"黑体\" color=blue size=4>\n",
    "1.使用线程可以把占据长时间的程序中的任务放到后台去处理。\n",
    "\n",
    "2.用户界面可以更加吸引人，比如用户点击了一个按钮去触发某些事件的处理，可以弹出一个进度条来显示处理的进度。\n",
    "\n",
    "3.程序的运行速度可能加快。\n",
    "\n",
    "4.在一些等待的任务实现上如用户输入、文件读写和网络收发数据等，线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。\n",
    "</font>\n",
    "\n",
    "每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行，必须依存在应用程序中，由应用程序提供多个线程执行控制。\n",
    "\n",
    "每个线程都有他自己的一组CPU寄存器，称为线程的上下文，该上下文反映了线程上次运行该线程的CPU寄存器的状态。\n",
    "\n",
    "**Python中使用线程有两种方式：函数或者用类来包装线程对象。**\n",
    "\n",
    "主要使用类包装，函数式使用在下方程序中。Python的标准库提供了两个模块：thread和threading，thread是低级模块，threading是高级模块，对_thread进行了封装。绝大多数情况下，我们只需要使用threading这个高级模块。\n",
    "\n",
    "1.函数式："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "-----主线程开始-----\n",
      "thread name is Thread-25thread name is Thread-26\n",
      "thread name is Thread-28\n",
      "thread name is Thread-27\n",
      "\n",
      "thread name is Thread-26\n",
      "thread name is Thread-28\n",
      "thread name is Thread-27thread name is Thread-25\n",
      "\n",
      "thread name is Thread-26\n",
      "thread name is Thread-28\n",
      "thread name is Thread-27thread name is Thread-25\n",
      "\n",
      "-----主线程结束-----\n"
     ]
    }
   ],
   "source": [
    "import threading,time\n",
    "def process():\n",
    "    for i in range(3):\n",
    "        time.sleep(1)\n",
    "        print(\"thread name is %s\" % threading.current_thread().name)\n",
    "if __name__ == '__main__':\n",
    "    print(\"-----主线程开始-----\")\n",
    "    threads = [threading.Thread(target=process) for i in range(4)] # 创建4个线程，存入列表\n",
    "    for t in threads:\n",
    "        t.start()                                  # 开启线程\n",
    "    for t in threads:\n",
    "        t.join()                                  # 等待子线程结束\n",
    "    print(\"-----主线程结束-----\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "2.OOP模式"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Thread start......\n",
      "0\n",
      "Thread start......\n",
      "1\n",
      "Thread start......\n",
      "2\n",
      "Thread start......\n",
      "3\n",
      "Thread start......\n",
      "Thread start......4Thread start......\n",
      "6\n",
      "\n",
      "\n",
      "5\n",
      "Thread start......\n",
      "7\n",
      "Thread start......\n",
      "8\n",
      "Thread start......\n",
      "9\n"
     ]
    }
   ],
   "source": [
    "#1.threading类\n",
    "import threading\n",
    "import time\n",
    "flag = 0\n",
    "class MyThread(threading.Thread):\n",
    "    \"\"\"自定义线程，继承父类\"\"\"\n",
    "    def __init__(self,threadid,threadname,counter):\n",
    "        threading.Thread.__init__(self)\n",
    "        \"\"\"定义构造函数，包括ID号、线程名称以及计数器,继承Thread类的构造函数\"\"\"\n",
    "        self.threadid = threadid\n",
    "        self.counter = counter\n",
    "        self.threadname  = threadname\n",
    "    def run(self):\n",
    "        \"\"\"关键：重写run()方法\"\"\"\n",
    "        print(\"Thread start......\")\n",
    "        print(self.threadname)\n",
    "        printValue(self.name,5,self.counter)\n",
    "        print(\"Quit thread......\")\n",
    "\n",
    "def printValue(threadname,delay,counter):\n",
    "    while counter >=0:\n",
    "        if flag:\n",
    "            threadname.exit()\n",
    "        time.sleep(delay)\n",
    "        print (\"%s: %s\" % (threadname, time.ctime(time.time())))\n",
    "        counter -= 1\n",
    "if __name__ =='__main__':\n",
    "    a = []\n",
    "    for i in range(10):\n",
    "        a.append(MyThread(i,i,i))\n",
    "        #添加2个新线程类，具体： 0 Thread-0 0\n",
    "    for j in range(10):\n",
    "        a[j].start()\n",
    "    for k in range(9,-1,-1):\n",
    "        a[k].join()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 上面使用了threading模块，同时自定义并继承了threading.Thread类\n",
    "\n",
    "线程的生命周期 ![](thread_state.jpg)\n",
    "\n",
    "同时开了10个线程，实现对数据的获取技术。\n",
    "\n",
    "join(timeout=None)方法将等待线程结束，或者是达到指定的timeout时间时。这种锁又称为自旋锁\n",
    "\n",
    "下面的作业要求：\n",
    "\n",
    "# 作业1：1.\t请完成一个多线程的程序\n",
    "\n",
    "实现五个线程同时向一个包含50个元素的列表中存储随机产生的1-6的整数。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "ThreadA 写入了数据 4\n",
      "ThreadB 写入了数据 2\n",
      "ThreadC 写入了数据 1\n",
      "ThreadDThreadE 写入了数据 6\n",
      " 写入了数据 3\n",
      "ThreadA 写入了数据 3\n",
      "ThreadB 写入了数据 2\n",
      "ThreadC 写入了数据 6\n",
      "ThreadE 写入了数据 4\n",
      "ThreadD 写入了数据 3\n",
      "ThreadA ThreadB 写入了数据写入了数据 6\n",
      " 4\n",
      "ThreadC 写入了数据 2\n",
      "ThreadE 写入了数据ThreadD 写入了数据 5\n",
      " 6\n",
      "ThreadAThreadB 写入了数据 4\n",
      " 写入了数据 5\n",
      "ThreadC 写入了数据 2\n",
      "ThreadDThreadE 写入了数据 5\n",
      " 写入了数据 6\n",
      "ThreadB 写入了数据 6\n",
      "ThreadA 写入了数据 2\n",
      "ThreadC 写入了数据 1\n",
      "ThreadE 写入了数据 6\n",
      "ThreadD 写入了数据 5\n",
      "ThreadB ThreadA写入了数据  写入了数据 4\n",
      "1\n",
      "ThreadC 写入了数据 2\n",
      "ThreadE 写入了数据 4\n",
      "ThreadD 写入了数据 1\n",
      "ThreadA ThreadB写入了数据 1\n",
      " 写入了数据 5\n",
      "ThreadC 写入了数据 4\n",
      "ThreadE 写入了数据 1\n",
      "ThreadD 写入了数据 2\n",
      "ThreadCThreadD 写入了数据 6\n",
      " 写入了数据 4\n",
      "ThreadAThreadBThreadE 写入了数据 6\n",
      " 写入了数据 1\n",
      " 写入了数据 4\n",
      "ThreadDThreadC 写入了数据 4\n",
      " 写入了数据 6\n",
      "ThreadBThreadA 写入了数据 5\n",
      " 写入了数据 4\n",
      "ThreadE 写入了数据 3\n",
      "ThreadDThreadC 写入了数据 2\n",
      " 写入了数据 2\n",
      "ThreadAThreadB 写入了数据 4\n",
      " 写入了数据 5\n",
      "ThreadE 写入了数据 1\n",
      "50\n",
      "[4, 2, 1, 3, 6, 3, 2, 6, 4, 3, 4, 6, 2, 6, 5, 5, 4, 2, 6, 5, 6, 2, 1, 6, 5, 1, 4, 2, 4, 1, 1, 5, 4, 1, 2, 4, 6, 6, 1, 4, 6, 4, 5, 4, 3, 2, 2, 4, 5, 1]\n"
     ]
    }
   ],
   "source": [
    "import threading\n",
    "import random\n",
    "import time\n",
    "a = []\n",
    "class MyTh(threading.Thread):\n",
    "    \"\"\"自定义线程，继承父类的Thread\"\"\"\n",
    "    def __init__(self,name):\n",
    "        threading.Thread.__init__(self)\n",
    "        self.name = name\n",
    "    def run(self):\n",
    "        while  len(a)<50:\n",
    "            r = random.randint(1,6)\n",
    "            a.append(r)\n",
    "            print(self.name,\"写入了数据\",r)\n",
    "            time.sleep(1)\n",
    "if __name__=='__main__':\n",
    "    thread1 = MyTh('ThreadA')\n",
    "    thread2 = MyTh('ThreadB')\n",
    "    thread3 = MyTh('ThreadC')\n",
    "    thread4 = MyTh('ThreadD')\n",
    "    thread5 = MyTh('ThreadE')\n",
    "    thread1.start()\n",
    "    thread2.start()\n",
    "    thread3.start()\n",
    "    thread4.start()\n",
    "    thread5.start()\n",
    "    thread1.join()\n",
    "    thread2.join()\n",
    "    thread3.join()\n",
    "    thread4.join()\n",
    "    thread5.join()  #把主线程让出，避免统计出错\n",
    "    threading.enumerate()  #获得当前所有活动对象（即run方法开始但是未终止的任何线程）的一个列表\n",
    "    print(len(a))\n",
    "    print(a)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "**python的多线程的问题：GIL导致PYTHON 无法使用到计算机的多核，仅能使用单核**\n",
    "\n",
    "# 进程和线程：\n",
    "\n",
    "下面先介绍Python的多线程和多进程模型。\n",
    "\n",
    "对于操作系统来说，一个任务就是一个进程（Process），比如打开一个浏览器就是启动一个浏览器进程.\n",
    "\n",
    "有些进程还不止同时干一件事，比如Word，它可以同时进行打字、拼写检查、打印等事情。在一个进程内部，要同时干多件事，就需要同时运行多个“子任务”，我们把进程内的这些“子任务”称为线程（Thread）。\n",
    "简而言之,一个程序至少有一个进程,一个进程至少有一个线程.\n",
    "\n",
    "​进程就是一个应用程序在处理机上的一次执行过程，它是一个动态的概念，而线程是进程中的一部分，进程包含多个线程在运行。\n",
    "\n",
    "​多线程可以共享全局变量，多进程不能。多线程中，所有子线程的进程号相同；多进程中，不同的子进程进程号不同。\n",
    "\n",
    "**并行处理**：是计算机系统中能同时执行两个或更多个处理的一种计算方法。并行处理可同时工作于同一程序的不同方面。并行处理的主要目的是节省大型和复杂问题的解决时间。\n",
    "\n",
    "**并发处理**：指一个时间段中有几个程序都处于已启动运行到运行完毕之间，且这几个程序都是在同一个处理机(CPU)上运行，但任一个时刻点上只有一个程序在处理机(CPU)上运行\n",
    "\n",
    "# GIL:\n",
    "\n",
    "首先需要明确的一点是 GIL 并不是Python的特性，它是在实现Python解析器(CPython)时所引入的一个概念。所以这里要先明确一点：GIL并不是Python的特性，Python完全可以不依赖于GIL\n",
    "\n",
    "那么CPython实现中的GIL又是什么呢？GIL全称 Global Interpreter Lock\n",
    "\n",
    "使用Python多线程的人都知道，Python中由于GIL(全局解释锁：Global Interpreter Lock)的存在，在多线程时并没有真正的进行多线程计算。\n",
    "\n",
    "GIL说白了就是伪多线程，一个线程运行其他线程阻塞，使你的多线程代码不是同时执行，而是交替执行。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total time: 130.5325391292572\n"
     ]
    }
   ],
   "source": [
    "from threading import Thread\n",
    "import time\n",
    "\n",
    "def my_counter():\n",
    "    i = 0\n",
    "    for _ in range(500000000):\n",
    "        i = i + 1\n",
    "    return True\n",
    "\n",
    "def main():\n",
    "    thread_array = {}\n",
    "    start_time = time.time()\n",
    "    for tid in range(2):\n",
    "        t = Thread(target=my_counter)\n",
    "        t.start()\n",
    "        t.join()\n",
    "    end_time = time.time()\n",
    "    print(\"Total time: {}\".format(end_time - start_time))\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    main()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total time: 122.67833018302917\n"
     ]
    }
   ],
   "source": [
    "from threading import Thread\n",
    "import time\n",
    "\n",
    "def my_counter():\n",
    "    i = 0\n",
    "    for _ in range(500000000):\n",
    "        i = i + 1\n",
    "    return True\n",
    "\n",
    "def main():\n",
    "    thread_array = {}\n",
    "    start_time = time.time()\n",
    "    for tid in range(2):\n",
    "        t = Thread(target=my_counter)\n",
    "        t.start()\n",
    "        thread_array[tid] = t\n",
    "    for i in range(2):\n",
    "        thread_array[i].join()\n",
    "    end_time = time.time()\n",
    "    print(\"Total time: {}\".format(end_time - start_time))\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    main()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "上面两个分别是单个线程处理和2个线程处理的情况，可以看到多线程反而慢了\n",
    "\n",
    "是否多线程就废了？当然不是，这里提一下IO密集型和计算密集型\n",
    "\n",
    "# 计算密集型\n",
    "\n",
    "计算密集型，顾名思义就是应用需要非常多的CPU计算资源，在计算密集型任务的特点是要进行大量的计算，消耗CPU资源，比如计算圆周率、对视频进行高清解码等等，全靠CPU的运算能力。\n",
    "\n",
    "# IO密集型\n",
    " 对于IO密集型的应用，涉及到网络、磁盘IO的任务都是IO密集型任务，大多消耗都是硬盘读写和网络传输的消耗。\n",
    "那么GIL多线程的不足，其实是对于计算密集型的不足，这个解决可以利用多进程进行解决，而对于IO密集型的任务，我们还是可以使用多多线程进行提升效率。\n",
    "\n",
    "线程是独立的。如果在一个线程中发生异常，则不会影响其他线程。它使用共享内存区域。\n",
    "\n",
    "多线程是一种执行模型，它允许多个线程存在于进程的上下文中，以便它们独立执行但共享其进程资源。\n",
    "\n",
    "# 线程锁机制"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "-------主线程开始------\n",
      "g_num is 100\n",
      "-------子线程1开始------\n",
      "g_num is 150\n",
      "-------子线程1结束------\n",
      "-------子线程2开始------\n",
      "g_num is 100\n",
      "-------子线程2结束------\n",
      "-------主线程结束------\n"
     ]
    }
   ],
   "source": [
    "from threading import Thread            # 导入线程\n",
    "import time\n",
    "def plus():                    # 第一个线程函数\n",
    "    print('-------子线程1开始------')\n",
    "    global g_num                    # 定义全局变量\n",
    "    g_num += 50                # 全局变量值加50\n",
    "    print('g_num is %d'%g_num)\n",
    "    print('-------子线程1结束------')\n",
    "def minus():                    # 第二个线程函数\n",
    "    time.sleep(1)                    # 休眠1毫秒\n",
    "    print('-------子线程2开始------')\n",
    "    global g_num                    # 定义全局变量\n",
    "    g_num -= 50                # 全局变量值减50\n",
    "    print('g_num is %d'%g_num)\n",
    "    print('-------子线程2结束------')\n",
    "g_num = 100                     # 定义一个全局变量\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    print('-------主线程开始------')\n",
    "    print('g_num is %d'%g_num)\n",
    "    t1 = Thread(target=plus)           # 实例化线程t1\n",
    "    t2 = Thread(target=minus)         # 实例化线程t2\n",
    "    t1.start()                          # 开启线程t1\n",
    "    t2.start()                          # 开启线程t2\n",
    "    t1.join()                           # 等待t1线程结束\n",
    "    t2.join()                           # 等待t2线程结束\n",
    "\n",
    "    print('-------主线程结束------')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "从上面的例子可以得出，在一个进程内的所有线程共享全局变量，能够在不使用其他方式的前提下完成多线程之间的数据共享。\n",
    "\n",
    "**下面我们进行加锁**\n",
    "\n",
    "多线程程序涉及到一个问题，那就是当不同线程要对同一个资源进行修改或利用时会出现混乱，所以有必要引入线程锁。\n",
    "由于线程可以对全局变量随意修改，这就可能造成多线程之间对全局变量的混乱操作。依然以房子为例，当房子内只有一个居住者时（单线程），他可以任意时刻使用任意一个房间，如厨房、卧室和卫生间等。但是，当这个房子有多个居住者时（多线程），他就不能在任意时刻使用某些房间，如卫生间，否则就会造成混乱。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "import threading\n",
    "from time import *\n",
    "\n",
    "class MyThread(threading.Thread):\n",
    "    def __init__(self,counter,name):\n",
    "        threading.Thread.__init__(self)\n",
    "        self.counter = counter\n",
    "        self.name = name\n",
    "\n",
    "    def run(self):\n",
    "        self.counter[0] += 1\n",
    "        sleep(1)\n",
    "        print(self.counter[0])\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    counter = [0]\n",
    "    for i in range(1,101):\n",
    "        t = MyThread(counter,i)\n",
    "        t.start()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "线程锁也称互斥锁，可以弥补部分线程安全问题。（线程锁和GIL锁是不一样的东西！）\n",
    "\n",
    "互斥锁为资源引入一个状态：锁定/非锁定\n",
    "\n",
    "某个线程要更改共享数据时，先将其锁定，此时资源的状态为“锁定”，其他线程不能更改；直到该线程释放资源，将资源的状态变成“非锁定”，其他的线程才能再次锁定该资源。\n",
    "\n",
    "**互斥锁保证了每次只有一个线程进行写入操作，从而保证了多线程情况下数据的正确性。**\n",
    "\n",
    "三种方法实现：获取锁、上锁、解锁\n",
    "\n",
    "<font face=\"黑体\" color=blue size=4>\n",
    "lock = threading.Lock()  # 取得锁\n",
    "\n",
    "lock.acquire()  # 上锁\n",
    "\n",
    "lock.release()  # 解锁\n",
    "</font>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "100100\n",
      "100\n",
      "100\n",
      "\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100100\n",
      "100\n",
      "100\n",
      "100\n",
      "1\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "100100\n",
      "100100100100\n",
      "100\n",
      "\n",
      "100100\n",
      "100\n",
      "\n",
      "100\n",
      "100\n",
      "100100100\n",
      "\n",
      "100\n",
      "100\n",
      "\n",
      "100\n",
      "\n",
      "100\n",
      "100\n",
      "\n",
      "\n",
      "100\n",
      "100\n",
      "100\n",
      "100100\n",
      "\n",
      "100\n",
      "100\n",
      "100\n",
      "100100\n",
      "100100\n",
      "\n",
      "100\n",
      "100\n",
      "100\n",
      "100100\n",
      "100\n",
      "100100\n",
      "100\n",
      "100\n",
      "100100\n",
      "\n",
      "\n",
      "100\n",
      "\n",
      "100\n",
      "100\n",
      "100\n",
      "100\n",
      "\n",
      "100100\n",
      "100\n",
      "\n",
      "2\n",
      "3\n",
      "4\n",
      "5\n",
      "6\n",
      "7\n",
      "8\n",
      "9\n",
      "10\n",
      "11\n",
      "12\n",
      "13\n",
      "14\n",
      "15\n",
      "16\n",
      "17\n",
      "18\n",
      "19\n",
      "20\n",
      "21\n",
      "22\n",
      "23\n",
      "24\n",
      "25\n",
      "26\n",
      "27\n",
      "28\n",
      "29\n",
      "30\n",
      "31\n",
      "32\n",
      "33\n",
      "34\n",
      "35\n",
      "36\n",
      "37\n",
      "38\n",
      "39\n",
      "40\n",
      "41\n",
      "42\n",
      "43\n",
      "44\n",
      "45\n",
      "46\n",
      "47\n",
      "48\n",
      "49\n",
      "50\n",
      "51\n",
      "52\n",
      "53\n",
      "54\n",
      "55\n",
      "56\n",
      "57\n",
      "58\n",
      "59\n",
      "60\n",
      "61\n",
      "62\n",
      "63\n",
      "64\n",
      "65\n",
      "66\n",
      "67\n",
      "68\n",
      "69\n",
      "70\n",
      "71\n",
      "72\n",
      "73\n",
      "74\n",
      "75\n",
      "76\n",
      "77\n",
      "78\n",
      "79\n",
      "80\n",
      "81\n",
      "82\n",
      "83\n",
      "84\n",
      "85\n",
      "86\n",
      "87\n",
      "88\n",
      "89\n",
      "90\n",
      "91\n",
      "92\n",
      "93\n",
      "94\n",
      "95\n",
      "96\n",
      "97\n",
      "98\n",
      "99\n"
     ]
    }
   ],
   "source": [
    "import threading\n",
    "from time import *\n",
    "\n",
    "class MyThread(threading.Thread):\n",
    "    def __init__(self,counter,name,lock):\n",
    "        threading.Thread.__init__(self)\n",
    "        self.counter = counter\n",
    "        self.name = name\n",
    "        self.lock = lock\n",
    "\n",
    "    def run(self):\n",
    "        self.lock.acquire()\n",
    "        self.counter[0] += 1\n",
    "        sleep(1)\n",
    "        print(self.counter[0])\n",
    "        self.lock.release()\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    counter = [0]\n",
    "    lock = threading.Lock()\n",
    "    for i in range(1,100):\n",
    "        t = MyThread(counter,i,lock)\n",
    "        t.start()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "# Threading 还有其他属性：\n",
    "\n",
    "Timer类，**Timer(int,target=func)**和Thread类类似，只不过它在int秒过后才以target指定的函数开始线程运行\n",
    "\n",
    "　　　　**currentThread()**　　获得当前线程对象\n",
    "\n",
    "　　　　**activeCount()**　　获得当前活动的线程总个数\n",
    "\n",
    "　　　　**enumerate()**　　获得所有活动线程的列表\n",
    "\n",
    "　　　　**settrace(func)**　　设置一跟踪函数，在run执行前执行\n",
    "\n",
    "　　　　**setprofile(func)**　　设置一跟踪函数，在run执行完毕之后执行\n",
    "\n",
    "# 作业2：2.\t生产者-消费者模型\n",
    "\n",
    "2.\t请完成一个多线程的程序，三个线程随机产生1-10的随机数放入一个包含10个元素的循环缓冲区，一个线程从里面取出元素并输出。其中循环缓冲区用自己定义的类。\n",
    "（请自定义栈的数据结构类实现）"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "ThreadA 写入数据： 2\t当前列表： [2]\n",
      "ThreadA 写入数据： 2\t当前列表： [2, 2]\n",
      "ThreadA 写入数据： 2\t当前列表： [2, 2, 2]\n",
      "ThreadA 写入数据： 7\t当前列表： [2, 2, 2, 7]\n",
      "ThreadA 写入数据： 9\t当前列表： [2, 2, 2, 7, 9]\n",
      "ThreadA 写入数据： 3\t当前列表： [2, 2, 2, 7, 9, 3]\n",
      "ThreadA 写入数据： 3\t当前列表： [2, 2, 2, 7, 9, 3, 3]\n",
      "ThreadA 写入数据： 2\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2]\n",
      "ThreadA 写入数据： 7\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7]\n",
      "ThreadA 写入数据： 10\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadA 写入数据： 6\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadA 写入数据： 1\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadA 写入数据： 6\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadA 写入数据： 6\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadA 写入数据： 3\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadA 写入数据： 1\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadA 写入数据： 5\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadB 写入数据： 5\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadB 写入数据： 10\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadB 写入数据： 2\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadB 写入数据： 5\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadB 写入数据： 8\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadB 写入数据： 8\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadB 写入数据： 10\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadB 写入数据： 5\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadB 写入数据： 4\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadB 写入数据： 5\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadB 写入数据： 8\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7, 10]\n",
      "ThreadC 读出数据： 10\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2, 7]\n",
      "ThreadC 读出数据： 7\t当前列表： [2, 2, 2, 7, 9, 3, 3, 2]\n",
      "ThreadC 读出数据： 2\t当前列表： [2, 2, 2, 7, 9, 3, 3]\n",
      "ThreadC 读出数据： 3\t当前列表： [2, 2, 2, 7, 9, 3]\n",
      "ThreadC 读出数据： 3\t当前列表： [2, 2, 2, 7, 9]\n",
      "ThreadC 读出数据： 9\t当前列表： [2, 2, 2, 7]\n",
      "ThreadC 读出数据： 7\t当前列表： [2, 2, 2]\n",
      "ThreadC 读出数据： 2\t当前列表： [2, 2]\n",
      "ThreadC 读出数据： 2\t当前列表： [2]\n",
      "ThreadC 读出数据： 2\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadB 写入数据： 5\t当前列表： [5]\n",
      "ThreadB 写入数据： 3\t当前列表： [5, 3]\n",
      "ThreadB 写入数据： 5\t当前列表： [5, 3, 5]\n",
      "ThreadB 写入数据： 8\t当前列表： [5, 3, 5, 8]\n",
      "ThreadB 写入数据： 3\t当前列表： [5, 3, 5, 8, 3]\n",
      "ThreadB 写入数据： 6\t当前列表： [5, 3, 5, 8, 3, 6]\n",
      "ThreadB 写入数据： 7\t当前列表： [5, 3, 5, 8, 3, 6, 7]\n",
      "ThreadB 写入数据： 10\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10]\n",
      "ThreadB 写入数据： 3\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10, 3]\n",
      "ThreadB 写入数据： 1\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10, 3, 1]\n",
      "ThreadB 写入数据： 3\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10, 3, 1]\n",
      "ThreadA 写入数据： 5\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10, 3, 1]\n",
      "ThreadA 写入数据： 3\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10, 3, 1]\n",
      "ThreadA 写入数据： 1\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10, 3, 1]\n",
      "ThreadA 写入数据： 2\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10, 3, 1]\n",
      "ThreadA 写入数据： 3\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10, 3, 1]\n",
      "ThreadA 写入数据： 10\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10, 3, 1]\n",
      "ThreadA 写入数据： 2\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10, 3, 1]\n",
      "ThreadA 写入数据： 2\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10, 3, 1]\n",
      "ThreadA 写入数据： 9\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10, 3, 1]\n",
      "ThreadA 写入数据： 6\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10, 3, 1]\n",
      "ThreadA 写入数据： 4\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10, 3, 1]\n",
      "ThreadA 写入数据： 8\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10, 3, 1]\n",
      "ThreadA 写入数据： 5\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10, 3, 1]\n",
      "ThreadC 读出数据： 1\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10, 3]\n",
      "ThreadC 读出数据： 3\t当前列表： [5, 3, 5, 8, 3, 6, 7, 10]\n",
      "ThreadC 读出数据： 10\t当前列表： [5, 3, 5, 8, 3, 6, 7]\n",
      "ThreadC 读出数据： 7\t当前列表： [5, 3, 5, 8, 3, 6]\n",
      "ThreadC 读出数据： 6\t当前列表： [5, 3, 5, 8, 3]\n",
      "ThreadB 写入数据： 5\t当前列表： [5, 3, 5, 8, 3, 5]\n",
      "ThreadB 写入数据： 9\t当前列表： [5, 3, 5, 8, 3, 5, 9]\n",
      "ThreadB 写入数据： 1\t当前列表： [5, 3, 5, 8, 3, 5, 9, 1]\n",
      "ThreadB 写入数据： 6\t当前列表： [5, 3, 5, 8, 3, 5, 9, 1, 6]\n",
      "ThreadB 写入数据： 2\t当前列表： [5, 3, 5, 8, 3, 5, 9, 1, 6, 2]\n",
      "ThreadB 写入数据： 5\t当前列表： [5, 3, 5, 8, 3, 5, 9, 1, 6, 2]\n",
      "ThreadB 写入数据： 4\t当前列表： [5, 3, 5, 8, 3, 5, 9, 1, 6, 2]\n",
      "ThreadB 写入数据： 5\t当前列表： [5, 3, 5, 8, 3, 5, 9, 1, 6, 2]\n",
      "ThreadC 读出数据： 2\t当前列表： [5, 3, 5, 8, 3, 5, 9, 1, 6]\n",
      "ThreadC 读出数据： 6\t当前列表： [5, 3, 5, 8, 3, 5, 9, 1]\n",
      "ThreadC 读出数据： 1\t当前列表： [5, 3, 5, 8, 3, 5, 9]\n",
      "ThreadC 读出数据： 9\t当前列表： [5, 3, 5, 8, 3, 5]\n",
      "ThreadC 读出数据： 5\t当前列表： [5, 3, 5, 8, 3]\n",
      "ThreadC 读出数据： 3\t当前列表： [5, 3, 5, 8]\n",
      "ThreadC 读出数据： 8\t当前列表： [5, 3, 5]\n",
      "ThreadC 读出数据： 5\t当前列表： [5, 3]\n",
      "ThreadC 读出数据： 3\t当前列表： [5]\n",
      "ThreadC 读出数据： 5\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n",
      "ThreadC 读出数据： None\t当前列表： []\n"
     ]
    }
   ],
   "source": [
    "import threading\n",
    "import time\n",
    "import random\n",
    "class Stack:\n",
    "    \"\"\"定义栈的数据结构\"\"\"\n",
    "    def __init__(self):\n",
    "        self.data = []\n",
    "    def isempty(self):\n",
    "        return len(self.data)==0\n",
    "    def __len__(self):\n",
    "        return len(self.data)\n",
    "    def push(self,element):\n",
    "        \"\"\"进栈\"\"\"\n",
    "        if not self.isfull():\n",
    "            self.data.append(element)\n",
    "    def top(self):\n",
    "        if self.isempty():\n",
    "            return None\n",
    "        else:\n",
    "            return self.data[-1]\n",
    "    def pop(self):\n",
    "        if self.isempty():\n",
    "            return None\n",
    "        else:\n",
    "            return self.data.pop()\n",
    "    def isfull(self):\n",
    "        return len(self.data) == 10\n",
    "    def printf(self):\n",
    "        return self.data\n",
    "\n",
    "stack = Stack()\n",
    "class ProduceThread(threading.Thread):\n",
    "    def __init__(self,name,lock):\n",
    "        threading.Thread.__init__(self)\n",
    "        self.name = name\n",
    "        self.lock = lock\n",
    "    def run(self) -> None:\n",
    "        point = 0\n",
    "        while point <30:\n",
    "            self.lock.acquire()\n",
    "            rnd = random.randint(1,10)\n",
    "            print(self.name,\"写入数据：\",rnd,end='\\t')\n",
    "            stack.push(rnd)\n",
    "            print(\"当前列表：\",stack.printf())\n",
    "            time.sleep(0.5)\n",
    "            self.lock.release()\n",
    "            point+=1\n",
    "\n",
    "class Consumed(threading.Thread):\n",
    "    def __init__(self,name,lock):\n",
    "        threading.Thread.__init__(self)\n",
    "        self.name = name\n",
    "        self.lock = lock\n",
    "    def run(self) -> None:\n",
    "        point = 0\n",
    "        while point <50:\n",
    "            self.lock.acquire()\n",
    "            #rnd = random.randint(1,10)\n",
    "            readout = stack.pop()\n",
    "            print(self.name,\"读出数据：\",readout,end='\\t')\n",
    "            print(\"当前列表：\",stack.printf())\n",
    "            time.sleep(0.5)\n",
    "            self.lock.release()\n",
    "            point+=1\n",
    "\n",
    "if __name__ =='__main__':\n",
    "    lock = threading.Lock()\n",
    "    thread1 = ProduceThread('ThreadA',lock)\n",
    "    thread2 = ProduceThread('ThreadB',lock)\n",
    "    thread3 = Consumed(\"ThreadC\",lock)\n",
    "    thread1.start()\n",
    "    thread2.start()\n",
    "    thread3.start()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "#生成的列表：\n",
    "stack.__len__()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "和Lock类类似的还有一个RLock类与Lock类的区别在于RLock类锁可以嵌套地acquire和release。也就是说在同一个线程中acquire之后再acquire也不会报错，而是将锁的层级加深一层。只有当每一层锁从下到上依次都release开这个锁才算是被解开。**RLock**锁也称递归锁\n",
    "\n",
    "# 这里还要提一下一个更强大的锁 Condition\n",
    "\n",
    "threading还补充了其他一些很多的带有锁功能的类。Condition就是其中最为强大的类之一。\n",
    "\n",
    "**acquire()**: 线程锁\n",
    "\n",
    "**release()**: 释放锁\n",
    "\n",
    "**wait(timeout)**: 线程挂起，直到收到一个notify通知或者超时（可选的，浮点数，单位是秒s）才会被唤醒继续运行。wait()必须在已获得Lock前提下才能调用，否则会触发RuntimeError。\n",
    "\n",
    "**notify(n=1)**: 通知其他线程，那些挂起的线程接到这个通知之后会开始运行，默认是通知一个正等待该condition的线程,最多则唤醒n个等待的线程。notify()必须在已获得Lock前提下才能调用，否则会触发RuntimeError。notify()不会主动释放Lock。\n",
    "\n",
    "**notifyAll()**: 如果wait状态线程比较多，notifyAll的作用就是通知所有线程。激活所有线程\n",
    "\n",
    "==============================================================================\n",
    "\n",
    "<font face=\"黑体\" color=blue size=4>\n",
    "注意释放锁relase是必要的，不然会出现死锁的现象！\n",
    "</font>\n",
    "\n",
    "下面是示例："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "68\n",
      "69\n",
      "1\n",
      "2\n",
      "3\n",
      "4\n",
      "5\n",
      "6\n",
      "7\n",
      "8\n",
      "9\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Exception in thread 84:\n",
      "Traceback (most recent call last):\n",
      "  File \"F:\\Anaconda\\envs\\python38\\lib\\threading.py\", line 932, in _bootstrap_inner\n",
      "    self.run()\n",
      "  File \"<ipython-input-16-67e56f14ea48>\", line 16, in run\n",
      "  File \"F:\\Anaconda\\envs\\python38\\lib\\threading.py\", line 352, in notify\n",
      "    raise RuntimeError(\"cannot notify on un-acquired lock\")\n",
      "RuntimeError: cannot notify on un-acquired lock\n"
     ]
    }
   ],
   "source": [
    "import threading\n",
    "from time import *\n",
    "\n",
    "class MyThread(threading.Thread):\n",
    "    def __init__(self,counter,name,con):\n",
    "        threading.Thread.__init__(self)\n",
    "        self.counter = counter\n",
    "        self.name = name\n",
    "        self.con = con\n",
    "\n",
    "    def run(self):\n",
    "        self.con.acquire()\n",
    "        self.counter[0] += 1\n",
    "        sleep(1)\n",
    "        print(self.counter[0])\n",
    "        con.notify()\n",
    "        con.wait()\n",
    "        self.con.release()\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    counter = [0]\n",
    "    con = threading.Condition()\n",
    "    for i in range(1,100):\n",
    "        t = MyThread(counter,i,con)\n",
    "        t.start()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 信号量类 BoundedSemaphore\n",
    "\n",
    "互斥锁同时只允许一个线程更改数据，而Semaphore信号量是同时允许一定数量的线程更改数据"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "55\n",
      "55\n",
      "\n",
      "5\n",
      "\n",
      "10\n",
      "10\n",
      "10\n",
      "12\n",
      "12\n",
      "151515\n",
      "15\n",
      "15\n",
      "\n",
      "\n",
      "20\n",
      "20\n",
      "20\n",
      "20\n",
      "23\n",
      "2525\n",
      "\n",
      "2626\n",
      "27\n",
      "\n",
      "30\n",
      "31\n",
      "323232\n",
      "\n",
      "\n",
      "35\n",
      "36\n",
      "373737\n",
      "\n",
      "\n",
      "4040\n",
      "\n",
      "424242\n",
      "\n",
      "\n",
      "4545\n",
      "\n",
      "47\n",
      "47\n",
      "47\n",
      "5050\n",
      "\n",
      "5252\n",
      "52\n",
      "\n",
      "55\n",
      "56\n",
      "57\n",
      "57\n",
      "57\n",
      "60\n",
      "61\n",
      "6262\n",
      "62\n",
      "\n",
      "65\n",
      "66\n",
      "67\n",
      "6768\n",
      "\n",
      "70\n",
      "70\n",
      "72\n",
      "7373\n",
      "\n",
      "7575\n",
      "\n",
      "77\n",
      "78\n",
      "78\n",
      "8080\n",
      "\n",
      "82\n",
      "8383\n",
      "\n",
      "8585\n",
      "\n",
      "87\n",
      "8888\n",
      "\n",
      "9090\n",
      "\n",
      "92\n",
      "9393\n",
      "\n",
      "95\n",
      "95\n",
      "97\n",
      "9898\n",
      "\n",
      "9999\n",
      "\n",
      "99\n",
      "99\n"
     ]
    }
   ],
   "source": [
    "import threading\n",
    "from time import *\n",
    "\n",
    "class MyThread(threading.Thread):\n",
    "    def __init__(self,counter,name):\n",
    "        threading.Thread.__init__(self)\n",
    "        self.counter = counter\n",
    "        self.name = name\n",
    "\n",
    "\n",
    "    def run(self):\n",
    "        semaphore.acquire()\n",
    "        self.counter[0] += 1\n",
    "        sleep(1)\n",
    "        print(self.counter[0])\n",
    "        semaphore.release()\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    counter = [0]\n",
    "    semaphore = threading.BoundedSemaphore(5)\n",
    "    for i in range(1,100):\n",
    "        t = MyThread(counter,i)\n",
    "        t.start()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "python中多线程无法利用多核优势，如果想要充分地使用多核cpu的资源（os.cpu_count()），在python中大部分情况需要使用多进程，python提供了multiprocessing。\n",
    "\n",
    "# 进程\n",
    "\n",
    "在Python中有多个模块可以创建进程，比较常用的有os.fork()函数、multiprocessing模块和Pool进程池。由于os.fork()函数只适用于Unix/Linux/Mac系统上运行，在Windows操作系统中不可用，所以本章重点介绍multiprocessing模块和Pool进程池这2个跨平台模块。\n",
    "\n",
    "**multiprocessing并非是python的一个模块，而是python中多进程管理的一个包**\n",
    "\n",
    "multiprocessing模块用来开启子进程，并在子进程中执行我们定制的任务（比如函数），该模块与多线程模块threading的编程接口类似。\n",
    "\n",
    "multiprocessing模块的功能众多：支持子进程，通信和共享数据，执行不同形式的同步，提供了process、Queue、Lock等组件。\n",
    "\n",
    "需要再次强调的一点是：与线程不同，进程没有任何共享状态，进程修改的数据，改动仅限与该进程内。使用方法\n",
    "\n",
    "# multiprocessing.Process([group [, target [, name [, args [, kwargs]]]]])\n",
    "\n",
    "同样，下面实现 使用multiprocessing模块创建进程"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "main process is running\n",
      "main process ends\n"
     ]
    }
   ],
   "source": [
    "from multiprocessing import Process\n",
    "def test():\n",
    "    print(\"son of process is running\")\n",
    "def main():\n",
    "    print(\"main process is running\")\n",
    "    p = Process(target=test,args=(1,))  #args：元素参数，targets函数名\n",
    "    p.start()\n",
    "    print('main process ends')\n",
    "if __name__ =='__main__':\n",
    "    main()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "Process的实例p常用的方法除start()外，还有如下常用方法：\n",
    "\n",
    "**is_alive()**：判断进程实例是否还在执行。\n",
    "\n",
    "**join([timeout])**：是否等待进程实例执行结束，或等待多少秒。\n",
    "\n",
    "**start()**：启动进程实例（创建子进程）。\n",
    "\n",
    "**run()**：如果没有给定target参数，对这个对象调用start()方法时，就将执行对象中的run()方法。\n",
    "\n",
    "**terminate()**：不管任务是否完成，立即终止。\n",
    "\n",
    "Process类还有如下常用属性：\n",
    "\n",
    "name：当前进程实例别名，默认为Process-N，N为从1开始递增的整数。\n",
    "\n",
    "pid：当前进程实例的PID值。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "------父进程开始执行-------\n",
      "父进程PID：17304\n",
      "p1.is_alive=True\n",
      "p2.is_alive=True\n",
      "p1.name=Process-9\n",
      "p1.pid=16248\n",
      "p2.name=mrsoft\n",
      "p2.pid=8500\n",
      "------等待子进程-------\n",
      "------父进程执行结束-------\n"
     ]
    }
   ],
   "source": [
    "from multiprocessing import Process\n",
    "import time\n",
    "import os\n",
    "# 两个子进程将会调用的两个方法\n",
    "def  child_1(interval):\n",
    "    print(\"子进程（%s）开始执行，父进程为（%s）\" % (os.getpid(), os.getppid()))\n",
    "    t_start = time.time()                   # 计时开始\n",
    "    time.sleep(interval)                    # 程序将会被挂起interval秒\n",
    "    t_end = time.time()                     # 计时结束\n",
    "    print(\"子进程（%s）执行时间为'%0.2f'秒\"%(os.getpid(),t_end - t_start))\n",
    "\n",
    "def  child_2(interval):\n",
    "    print(\"子进程（%s）开始执行，父进程为（%s）\" % (os.getpid(), os.getppid()))\n",
    "    t_start = time.time()                   # 计时开始\n",
    "    time.sleep(interval)                    # 程序将会被挂起interval秒\n",
    "    t_end = time.time()                     # 计时结束\n",
    "    print(\"子进程（%s）执行时间为'%0.2f'秒\"%(os.getpid(),t_end - t_start))\n",
    "\n",
    "if __name__ == '__main__':\n",
    "    print(\"------父进程开始执行-------\")\n",
    "    print(\"父进程PID：%s\" % os.getpid())                 # 输出当前程序的PID\n",
    "    p1=Process(target=child_1,args=(1,))                 # 实例化进程p1\n",
    "    p2=Process(target=child_2,name=\"mrsoft\",args=(2,))    # 实例化进程p2\n",
    "    p1.start()                          # 启动进程p1\n",
    "    p2.start()                          # 启动进程p2\n",
    "    # 同时父进程仍然往下执行，如果p2进程还在执行，将会返回True\n",
    "    print(\"p1.is_alive=%s\"%p1.is_alive())\n",
    "    print(\"p2.is_alive=%s\"%p2.is_alive())\n",
    "    # 输出p1和p2进程的别名和PID\n",
    "    print(\"p1.name=%s\"%p1.name)\n",
    "    print(\"p1.pid=%s\"%p1.pid)\n",
    "    print(\"p2.name=%s\"%p2.name)\n",
    "    print(\"p2.pid=%s\"%p2.pid)\n",
    "    print(\"------等待子进程-------\")\n",
    "    p1.join()                         # 等待p1进程结束\n",
    "    p2.join()                         # 等待p2进程结束\n",
    "    print(\"------父进程执行结束-------\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "pycharm": {
     "name": "#%%\n"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "------父进程开始执行-------\n",
      "父进程PID：15180\n",
      "p1.is_alive=True\n",
      "p2.is_alive=True\n",
      "p1.name=mrsoft\n",
      "p1.pid=18484\n",
      "p2.name=SubProcess-2\n",
      "p2.pid=20300\n",
      "------等待子进程-------\n",
      "------父进程执行结束-------\n"
     ]
    }
   ],
   "source": [
    "#下面的代码实现进程类Processor的处理\n",
    "from multiprocessing import Process\n",
    "import time\n",
    "import os\n",
    "# 继承Process类\n",
    "class SubProcess(Process):\n",
    "    # 由于Process类本身也有__init__初始化方法，这个子类相当于重写了父类的这个方法\n",
    "    def __init__(self,interval,name=''):\n",
    "        Process.__init__(self)       # 调用Process父类的初始化方法\n",
    "        self.interval = interval     # 接收参数interval\n",
    "        if name:                     # 判断传递的参数name是否存在\n",
    "            self.name = name    # 如果传递参数name,则为子进程创建name属性，否则使用默认属性\n",
    "    # 重写了Process类的run()方法\n",
    "\n",
    "    def run(self):\n",
    "        print(\"子进程(%s) 开始执行，父进程为（%s）\"%(os.getpid(),os.getppid()))\n",
    "        t_start = time.time()\n",
    "        time.sleep(self.interval)\n",
    "        t_stop = time.time()\n",
    "        print(\"子进程(%s)执行结束，耗时%0.2f秒\"%(os.getpid(),t_stop-t_start))\n",
    "\n",
    "if __name__==\"__main__\":\n",
    "    print(\"------父进程开始执行-------\")\n",
    "    print(\"父进程PID：%s\" % os.getpid())        # 输出当前程序的ID\n",
    "    p1 = SubProcess(interval=1,name='mrsoft')\n",
    "    p2 = SubProcess(interval=2)\n",
    "    # 对一个不包含target属性的Process类执行start()方法，就会运行这个类中的run()方法\n",
    "    # 所以这里会执行p1.run()\n",
    "    p1.start()                      # 启动进程p1\n",
    "    p2.start()                      # 启动进程p2\n",
    "    # 输出p1和p2进程的执行状态，如果真正进行，返回True；否则返回False\n",
    "    print(\"p1.is_alive=%s\"%p1.is_alive())\n",
    "    print(\"p2.is_alive=%s\"%p2.is_alive())\n",
    "    # 输出p1和p2进程的别名和PID\n",
    "    print(\"p1.name=%s\"%p1.name)\n",
    "    print(\"p1.pid=%s\"%p1.pid)\n",
    "    print(\"p2.name=%s\"%p2.name)\n",
    "    print(\"p2.pid=%s\"%p2.pid)\n",
    "    print(\"------等待子进程-------\")\n",
    "    p1.join()                     # 等待p1进程结束\n",
    "    p2.join()                     # 等待p2进程结束\n",
    "    print(\"------父进程执行结束-------\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "上述代码中，定义了一个SubProcess子类，继承multiprocess.Process父类。\n",
    "SubProcess子类中定义了2个方法：**__init__()**初始化方法和**run()**方法。在__init()__初识化方法中，调用multiprocess.Process父类的__init__()初始化方法，\n",
    "否则父类初始化方法会被覆盖，无法开启进程。\n",
    "此外，在**SubProcess**子类中并没有定义start()方法，\n",
    "但在主进程中却调用了start()方法，此时就会自动执行SubPorcess类的run()方法。\n",
    "\n",
    "# 使用进程池Pool创建进程\n",
    "\n",
    "如果要创建几十个或者上百个进程，则需要实例化更多个Process类。有没有更好的创建进程的方式解决这类问题呢?\n",
    "\n",
    "当然，像上面一样建立一个大列表去append也是可以的，但是如果进程太多了呢？？\n",
    "\n",
    "这时候我们需要考虑进程池了\n",
    "\n",
    "为了更好的理解进程池，可以将进程池比作水池，如图所示。我们需要完成放满10个水盆的水的任务，而在这个水池中，最多可以安放3个水盆接水，也就是同时可以执行3个任务，即开启3个进程。为更快完成任务，现在打开3个水龙头开始放水，当有一个水盆的水接满时，\n",
    "即该进程完成1个任务，我们就将这个水盆的水倒入水桶中，然后继续接水，即执行下一个任务。\n",
    "\n",
    "如果3个水盆每次同时装满水，那么在放满第9盆水后，系统会随机分配1个水盆接水，另外2个水盆空闲。\n",
    "![](Pool.jpg)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "is_executing": true,
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "import os\n",
    "import time\n",
    "#下面尝试设计进程池的处理技术\n",
    "from multiprocessing import Pool\n",
    "def task(name):\n",
    "    print('子进程（%s）执行task %s ...' % (os.getpid() ,name))\n",
    "    time.sleep(1)                       # 休眠1秒\n",
    "if __name__=='__main__':\n",
    "    print('父进程（%s）.' % os.getpid())\n",
    "    p = Pool(5)                    # 定义一个进程池，最大进程数3\n",
    "    for i in range(10):                       # 从0开始循环10次\n",
    "        p.apply_async(task, args=(i,))      # 使用非阻塞方式调用task()函数\n",
    "    print('等待所有子进程结束...')\n",
    "    p.close()                       # 关闭进程池，关闭后p不再接收新的请求\n",
    "    p.join()                        # 等待子进程结束\n",
    "    print('所有子进程结束.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "# p.apply() 和 p.apply_async() 方法\n",
    "apply方法是阻塞的。\n",
    "意思就是等待当前子进程执行完毕后，才执行下一个进程。\n",
    "\n",
    "因为apply是阻塞的，所以进入子进程执行后，等待当前子进程执行完毕，在继续执行下一个进程。\n",
    "\n",
    "apply_async 是异步非阻塞的。\n",
    "意思就是：不用等待当前进程执行完毕，随时根据系统调度来进行进程切换。\n",
    "\n",
    "**map()方法**\n",
    "\n",
    "函数原型：map(func, iterable[, chunksize=None])\n",
    "\n",
    "Pool类中的map方法，与内置的map函数用法行为基本一致，它会使进程阻塞直到结果返回。\n",
    "\n",
    "**pool.apply_async(func, args=(), kwds={}, callback=None, error_callback=None)**\n",
    "\n",
    "如：pool.apply_async(func1, args=(10,), callback=func2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "# 多线程与图形用户界面(GUI) 主要讲wxPython PyQt5或者Tkinter的方法请自查\n",
    "python3多线程threading功能强大，结合join()和setDaemon()函数使用更灵活，使用多线程时首先想到了threading， 但wxpython是用python写的一个UI框架，其本身自带有多线程函数：\n",
    "\n",
    "**wx.PostEvent**\n",
    "\n",
    "**wx.CallAfter**\n",
    "\n",
    "**wx.CallLater**\n",
    "\n",
    "舍去这三个，直接使用threading导致UI出现卡顿，卡死等情况。结合wxpython自带的多线程函数效果很好。\n",
    "\n",
    "2. 使用多线程做UI时，建议主线程进行UI相关的显示数据绘制，而且子线程只进行逻辑处理。例如：wxPython中子线程调用方法wx.CallAfter()之后，主线程的UI可以在当前事件处理结束后处理子线程发送的消息,这是wxPython中子线程给主线程发送消息最简单的方法。\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "ename": "PyNoAppError",
     "evalue": "The wx.App object must be created first!",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mPyNoAppError\u001b[0m                              Traceback (most recent call last)",
      "\u001b[1;32m<ipython-input-13-9f48e95c4400>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m     64\u001b[0m \u001b[1;32mif\u001b[0m \u001b[0m__name__\u001b[0m \u001b[1;33m==\u001b[0m \u001b[1;34m\"__main__\"\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     65\u001b[0m     \u001b[0mapp\u001b[0m \u001b[1;33m=\u001b[0m \u001b[0mwx\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mApp\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;32mFalse\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 66\u001b[1;33m     \u001b[0mAppUI\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;32mNone\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mShow\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m     67\u001b[0m     \u001b[0mapp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mMainLoop\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     68\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n",
      "\u001b[1;32m<ipython-input-13-9f48e95c4400>\u001b[0m in \u001b[0;36m__init__\u001b[1;34m(self, parent, title)\u001b[0m\n\u001b[0;32m     38\u001b[0m \u001b[1;32mclass\u001b[0m \u001b[0mAppUI\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mwx\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mFrame\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     39\u001b[0m     \u001b[1;32mdef\u001b[0m \u001b[0m__init__\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mparent\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtitle\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;34m\"test thread\"\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m---> 40\u001b[1;33m         \u001b[0mwx\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mFrame\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m__init__\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mparent\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mtitle\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mtitle\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m     41\u001b[0m         \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mCenter\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m     42\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n",
      "\u001b[1;31mPyNoAppError\u001b[0m: The wx.App object must be created first!"
     ]
    }
   ],
   "source": [
    "import wx\n",
    "import time\n",
    "import threading\n",
    "\n",
    "class ProgressBarThread(threading.Thread):\n",
    "    \"\"\"进度条类  \"\"\"\n",
    "    def __init__(self, parent):\n",
    "        \"\"\"\n",
    "        :param parent:  主线程UI\n",
    "        :param timer:  计时器\n",
    "        \"\"\"\n",
    "        super(ProgressBarThread, self).__init__()  # 继承\n",
    "        self.parent = parent\n",
    "        self.setDaemon(True)  # 设置为守护线程， 即子线程是守护进程，主线程结束子线程也随之结束。\n",
    "\n",
    "    def run(self):\n",
    "        count = 0\n",
    "        while count < 5:\n",
    "            count = count + 0.5\n",
    "            time.sleep(0.5)\n",
    "            wx.CallAfter(self.parent.update_process_bar, count)  # 更新进度条进度\n",
    "        wx.CallAfter(self.parent.close_process_bar)  #  destroy进度条\n",
    "\n",
    "class CounterThread(threading.Thread):\n",
    "    \"\"\"计时类\"\"\"\n",
    "    def __init__(self, parent):\n",
    "        super(CounterThread, self).__init__()\n",
    "        self.parent = parent\n",
    "        self.setDaemon(True)\n",
    "\n",
    "    def run(self):\n",
    "        i = 0\n",
    "        while i < 5:\n",
    "            i += 1\n",
    "            print(i)\n",
    "            time.sleep(1)\n",
    "\n",
    "class AppUI(wx.Frame):\n",
    "    def __init__(self, parent, title=\"test thread\"):\n",
    "        wx.Frame.__init__(self, parent, title=title)\n",
    "        self.Center()\n",
    "\n",
    "        self.panel = wx.Panel(parent=self)\n",
    "        self.button_panel = wx.Panel(self.panel)\n",
    "        self.button_start = wx.Button(self.panel, wx.ID_ANY, label=\"开始\")\n",
    "\n",
    "        self.button_start.Bind(wx.EVT_BUTTON, self.on_click_start)\n",
    "        self.dialog = None\n",
    "\n",
    "    def update_process_bar(self, count):\n",
    "        self.dialog.Update(count)\n",
    "\n",
    "    def close_process_bar(self):\n",
    "        self.dialog.Destroy()\n",
    "\n",
    "    def on_click_start(self, evt):\n",
    "        self.dialog = wx.ProgressDialog(\"录音进度\", \"Time remaining\", 5, style=wx.PD_AUTO_HIDE | wx.PD_ELAPSED_TIME | wx.PD_REMAINING_TIME)\n",
    "        self.progress_1 = ProgressBarThread(self)\n",
    "        self.progress_1.start()\n",
    "        self.counter = CounterThread(self)\n",
    "        self.counter.start()\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    app = wx.App(False)\n",
    "    AppUI(None).Show()\n",
    "    app.MainLoop()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "pycharm": {
     "name": "#%% md\n"
    }
   },
   "source": [
    "注意，上面这段代码只能在CMD窗口运行或者写入Py文件或者在Pycharm 中运行，否则会报错！\n",
    "\n",
    "# 进程队列通信技术\n",
    "\n",
    "Python的multiprocessing模块包装了底层的机制，提供了Queue（队列）、Pipes（管道）等多种方式来交换数据\n",
    "\n",
    "进程之间有时需要通信，操作系统提供了很多机制来实现进程间的通信。可以使用multiprocessing模块的Queue实现多进程之间的数据传递。Queue本身是一个消息列队程序，下面介绍一下Queue的使用。\n",
    "\n",
    "初始化Queue()对象时（例如：q=Queue(num)），若括号中没有指定最大可接收的消息数量，或数量为负值，那么就代表可接受的消息数量没有上限（直到内存的尽头）。Queue的常用方法如下：\n",
    "\n",
    "##  Queue.qsize()：返回当前队列包含的消息数量。\n",
    "\n",
    "##  Queue.empty()：如果队列为空，返回True；反之返回False 。\n",
    "\n",
    "##  Queue.full()：如果队列满了，返回True；反之返回False。\n",
    "\n",
    "## Queue.get([block[, timeout]])：获取队列中的一条消息，然后将其从列队中移除，block默认值为True。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "pycharm": {
     "is_executing": true,
     "name": "#%%\n"
    }
   },
   "outputs": [],
   "source": [
    "# 使用进程API里面的Queue数据结构 实现读写机制\n",
    "from multiprocessing import Process, Queue\n",
    "import  time\n",
    "# 向队列中写入数据\n",
    "def write_task(q):\n",
    "    if not q.full():\n",
    "        for i in range(5):\n",
    "            message = \"消息\" + str(i)\n",
    "            q.put(message)\n",
    "            print(\"写入:%s\"%message)\n",
    "\n",
    "# 从队列读取数据\n",
    "def read_task(q):\n",
    "    time.sleep(1) # 休眠1秒\n",
    "    while not q.empty():\n",
    "        print(\"读取:%s\" % q.get(True,2))     # 等待2秒，如果还没读取到任何消息，\n",
    "                                             # 则抛出\"Queue.Empty\"异常\n",
    "if __name__ == \"__main__\":\n",
    "    print(\"-----父进程开始-----\")\n",
    "    q = Queue()                      # 父进程创建Queue，并传给各个子进程\n",
    "    pw = Process(target=write_task, args=(q,))    # 实例化写入队列的子进程，并且传递队列\n",
    "    pr = Process(target=read_task, args=(q,))    # 实例化读取队列的子进程，并且传递队列\n",
    "    pw.start()                       # 启动子进程pw，写入\n",
    "    pr.start()                       # 启动子进程pr，读取\n",
    "    pw.join()                        # 等待pw结束\n",
    "    pr.join()                       # 等待pr结束\n",
    "    print(\"-----父进程结束-----\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "PyCharm (PycharmWorkspace)",
   "language": "python",
   "name": "pycharm-7b63a738"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
